Camera

Make use of the device camera to take and serialize a photo, display it as thumbnail and create a simple single-photo gallery.

Preview

In this lab you will use the device camera to take a photo.

  • The photo may be saved to a file
  • It will appear as a thumbnail in the residence details view
  • An enlarged image will be viewable in a one-photo gallery

The following figures provide an outline.

Figure 1: Set up Genymotion camera

Figure 2: Start the picture-taking process

Figure 3: Take and save photo

Figure 4: Long-press thumbnail opens gallery

Resources

Manifest

Proceeding with the application as completed to the end of the previous lab, we shall add two new activities, the content for which is provided in a later step:

  • ResidenceCameraActivity
    • Manages the taking of a photo
  • ResidenceGalleryActivity
    • Allows the display of the photo in a single-photo gallery.

Modify manifest file: add following snippets.

    <activity
        android:name=".activities.ResidenceCameraActivity"
        android:label="@string/app_name">

        <meta-data android:name="android.support.PARENT_ACTIVITY"
                   android:value=".activities.ResidencePagerActivity"/>
    </activity>
    <activity
        android:name=".activities.ResidenceGalleryActivity"
        android:label="@string/app_name">

        <meta-data android:name="android.support.PARENT_ACTIVITY"
                   android:value=".activities.ResidencePagerActivity"/>
    </activity>

Residence details view layout

We add a Camera button and a thumnail placeholder to the Residence details view:

  • layout/fragment_residence.xml

Figure 1: Camera button and thumbnail placeholder

Figure 2: Camera button and thumbnail placeholder positions

First add 2 string resources:

  <string name="take_photo">Take Photo</string>
  <string name="save_photo">Save Photo</string>

Here is the additional code:

<!-- Camera button + thumbnail-->
      <ImageButton
          android:id="@+id/camera_button"
          android:layout_width="40dp"
          android:layout_height="40dp"
          android:scaleType="centerInside"
          android:layout_marginTop="16dp"
          android:src="@android:drawable/ic_menu_camera"
          android:layout_gravity="top"/>
      <ImageView
          android:id="@+id/myrent_imageView"
          android:layout_width="40dp"
          android:layout_height="40dp"
          android:scaleType="centerInside"
          android:background="@android:color/darker_gray"
          android:cropToPadding="true"
          android:layout_marginTop="16dp"
          android:layout_gravity="top|end"
          android:layout_margin="16dp"/>

As you can see, the code is located immediately following the geolocation input window.

For reference, here is the refactored fragment_residence.xml file:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical" >

  <!-- LOCATION -->

  <TextView
      style="?android:listSeparatorTextViewStyle"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/location" />

  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="51dp"
      android:baselineAligned="false"
      android:orientation="horizontal"
      android:weightSum="1">

    <!-- Geolocation (GPS Coords) -->

    <EditText
        android:id="@+id/geolocation"
        android:layout_width="194dp"
        android:layout_height="wrap_content"
        android:hint="@string/geolocation_hint"
        android:layout_weight="0.75">

      <requestFocus />
    </EditText>

    <!-- Camera button + thumbnail-->
      <ImageButton
          android:id="@+id/camera_button"
          android:layout_width="40dp"
          android:layout_height="40dp"
          android:scaleType="centerInside"
          android:layout_marginTop="16dp"
          android:src="@android:drawable/ic_menu_camera"
          android:layout_gravity="top"/>
      <ImageView
          android:id="@+id/myrent_imageView"
          android:layout_width="40dp"
          android:layout_height="40dp"
          android:scaleType="centerInside"
          android:background="@android:color/darker_gray"
          android:cropToPadding="true"
          android:layout_marginTop="16dp"
          android:layout_gravity="top|end"
          android:layout_margin="16dp"/>

  </LinearLayout>

  <!-- STATUS -->

  <TextView
      style="?android:listSeparatorTextViewStyle"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/status" />

  <Button android:id="@+id/registration_date"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:layout_marginLeft="16dp"
          android:layout_marginRight="16dp"
      />
  <!-- Checkbox -->
  <CheckBox
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/isrented"
      android:id="@+id/isrented"
      android:checked="false"/>

  <Button
      android:id="@+id/tenant"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_marginLeft="16dp"
      android:layout_marginRight="16dp"
      android:text="@string/landlord" />

  <Button android:id="@+id/residence_reportButton"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:layout_marginLeft="16dp"
          android:layout_marginRight="16dp"
          android:text="@string/residence_report"
      />


  <android.support.design.widget.FloatingActionButton
      android:id="@+id/fab"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="bottom|end"
      android:layout_margin="@dimen/fab_margin"
      android:src="@drawable/ic_blue_marker"/>
</LinearLayout>

Camera layout

Next we add a layout for the Camera activity:

File: /res/layout/residence_photo.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingBottom="@dimen/activity_vertical_margin"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                tools:context="org.wit.myrent.activities.ResidenceCameraActivity" >

  <ImageView
      android:id="@+id/residenceImage"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_above="@+id/takePhoto"
      android:layout_alignParentRight="true"
      android:layout_alignParentTop="true"
      android:src="@mipmap/ic_lens"/>

  <Button
      android:id="@+id/takePhoto"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/residenceImage"
      android:layout_alignParentBottom="true"
      android:text="@string/take_photo" />

  <Button
      android:id="@+id/savePhoto"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBottom="@+id/takePhoto"
      android:layout_alignRight="@+id/residenceImage"
      android:layout_alignTop="@+id/takePhoto"
      android:text="@string/save_photo" />

</RelativeLayout>

Download the image ic_lens.png from here to a temp location.

  • In Android Studio select res/mipmap, right click and select new|Image Asset.
  • The Configure Image Asset window should open.
  • Change the name to ic_lens.
  • Browse to where you have saved the ic_lens image and select it.
  • The result should be as represented in Figure 3.
  • Overwrite ic_lens if it already exist.
  • Complete the remaining steps in the configuration wizard.

Figure 3: Image Asset configuration

Figure 4: Layout for Camera activity

Emulator

If using an Android emulator ensure you enable at least one camera simulator in the settings. This is illustrated in Figure 5.

Figure 5: Enable Emulator camera

Model

The model Residence class requires a new field to represent the photo filename:

  • Add and initialize with a placeholder name to avoid null pointer issues.
public String photo;
public Residence()
{
  ...
  photo = "photo";
}

For reference here is the updated Residence:

package org.wit.myrent.models;

import android.content.Context;

import org.wit.myrent.R;

import java.util.Date;
import java.util.Random;


public class Residence
{
  public Long id;
  public Long date;

  //a latitude longitude pair
  //example "52.4566,-6.5444"
  public String geolocation;
  public boolean rented;
  public String tenant;
  public double  zoom       ;//zoom level of accompanying map
  public String photo;

  public Residence() {
    id = unsignedLong();
    date = new Date().getTime();
    geolocation = "52.253456,-7.187162";
    tenant = "none presently";
    zoom = 16.0f;
    photo = "photo";
  }

  public void setGeolocation(String geolocation) {
    this.geolocation = geolocation;
  }

  public String getGeolocation() {
    return geolocation;
  }

  public String getDateString() {
    return "Registered:" + dateString();
  }

  private String dateString() {
    String dateFormat = "EEE d MMM yyyy H:mm";
    return android.text.format.DateFormat.format(dateFormat, date).toString();
  }

  public String getResidenceReport(Context context) {
    String rentedString = "";
    if (rented) {
      rentedString = context.getString(R.string.residence_report_rented);
    }
    else {
      rentedString = context.getString(R.string.residence_report_not_rented);
    }

    String prospectiveTenant = tenant;
    if (tenant == null) {
      prospectiveTenant = context.getString(R.string.residence_report_nobody_interested);
    }
    else {
      prospectiveTenant = context.getString(R.string.residence_report_prospective_tenant, tenant);
    }
    String report = "Location " + geolocation + " Date: " + dateString() + " " + rentedString + " " + prospectiveTenant;
    return report;

  }

  /**
   * Generate a long greater than zero
   * @return Unsigned Long value greater than zero
   */
  private Long unsignedLong() {
    long rndVal = 0;
    do {
      rndVal = new Random().nextLong();
    } while (rndVal <= 0);
    return rndVal;
  }
}

Helpers

We will now add two helper classes, CameraHelper & FileIOHelper.

  • CameraHelper class has 2 methods:

    • showPhoto
      • displays the photo
    • getScaledDrawable
      • scales photo to fit in the application's imageview

    These methods have been obtained from Android Programming by Hardy & Phillips.

CameraHelper

File: org.wit.android.helpers.CameraHelper.java

package org.wit.android.helpers;

import java.util.List;

import org.wit.myrent.models.Residence;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.hardware.Camera.Size;
import android.view.Display;
import android.widget.ImageView;

public class CameraHelper
{

  /**
   * Render the photo on the ImageView
   */
  public static void showPhoto(Activity activity, Residence res, ImageView photoView)
  { 
    String path = activity.getFileStreamPath(res.photo).getAbsolutePath();
    BitmapDrawable b = getScaledDrawable(activity, path);
    if (b != null)
      photoView.setImageDrawable(b);
  }

  /**
   * Get a BitmapDrawable from a local file that is scaled down to fit the
   * current Window size.
   */
  @SuppressWarnings("deprecation")
  public static BitmapDrawable getScaledDrawable(Activity a, String path)
  {
    Display display = a.getWindowManager().getDefaultDisplay();
    float destWidth = display.getWidth();
    float destHeight = display.getHeight();

    // read in the dimensions of the image on disk
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(path, options);

    float srcWidth = options.outWidth;
    float srcHeight = options.outHeight;

    int inSampleSize = 1;
    if (srcHeight > destHeight || srcWidth > destWidth)
    {
      if (srcWidth > srcHeight)
      {
        inSampleSize = Math.round((float) srcHeight / (float) destHeight);
      }
      else
      {
        inSampleSize = Math.round((float) srcWidth / (float) destWidth);
      }
    }

    options = new BitmapFactory.Options();
    options.inSampleSize = inSampleSize;

    Bitmap bitmap = BitmapFactory.decodeFile(path, options);
    return new BitmapDrawable(a.getResources(), bitmap);
  }

}

FileIOHelper

Here is the FileIOHelper class:

package org.wit.android.helpers;

import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;

import android.content.Context;
import android.graphics.Bitmap;

public class FileIOHelper
{
  public static boolean write(Context context, String filename, byte[] data)
  {
    FileOutputStream os = null;
    boolean success = true;
    try
    {
      os = context.openFileOutput(filename, Context.MODE_PRIVATE);      
      os.write(data);
    }
    catch (Exception e)
    {
      LogHelpers.info(context,"Error writing to file " + filename + " " + e.getMessage());
      success = false;
    }
    finally
    {
      try
      {
        if (os != null)
          os.close();
      }
      catch (Exception e)
      {
        LogHelpers.info(context, "Error closing file " + filename + " " + e.getMessage());
        success = false;
      }
    }
    return success;
  }

  public static byte[] byteArray(Bitmap bmp)
  {
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
    return stream.toByteArray();
  }

  public static boolean writeBitmap(Context context, String filename, Bitmap bmp)
  {
    return write(context, filename, byteArray(bmp));
  }
}

Camera Activity

Note: Not all necessary import statements are supplied as accompaniment to the various code snipptes below. A complete set of statements is provided at the end of this file for reference purposes.

Create a new activity ResidenceCameraActivity in the package org.wit.myrent.activities.

package org.wit.myrent.activities;

import org.wit.myrent.R;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class ResidenceCameraActivity extends AppCompatActivity
{

  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.residence_photo);
  }

}

Introduce and wire up the Up button so that it behaves similarly to the back button. We can do this by invoking Activity.onBackPressed

  • Display and enable the Up button by adding this statement to the onCreate method following setContentView:
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
  • Override onOptionsItemSelected to act on pressing the Up button:
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
      case android.R.id.home:
        onBackPressed();
        return true;

      default:
        return super.onOptionsItemSelected(item);
    }
  }

Add an import statement for MenuItem:

import android.view.MenuItem;

There are two buttons in the view: Take Photo and Save Photo.

  • Add fields for these
  private Button    savePhoto;
  private Button    takePhoto;

Import the Button class:

import android.widget.Button;

Add an ImageView field in which the photo will be displayed.

  private ImageView residenceImage;

Import the ImageView class:

import android.widget.ImageView;

Bind the widget instances to their respective layout elements. The following code which should be added to onCreate does this:

    residenceImage  = (ImageView) findViewById(R.id.residenceImage);
    savePhoto       = (Button)findViewById(R.id.savePhoto);
    takePhoto       = (Button)findViewById(R.id.takePhoto);

Set the default state of the save photo button to disabled. We will only enable this once a photo has been taken.

    savePhoto.setEnabled(false);

Implement an OnClickListener interface:

public class ResidenceCameraActivity extends AppCompatActivity implements OnClickListener

Import the listener interface:

import android.view.View.OnClickListener;

Add the listener method skeleton:

  @Override
  public void onClick(View v)
  {
    // TODO Auto-generated method stub

  }

Register listeners in onCreate to respond to button clicks:

    savePhoto.setOnClickListener(this);
    takePhoto.setOnClickListener(this);

In onClick method respond to clicking the buttons. For convenience we will locate most of the necessary code, as shown below, in two private methods:

  • onTakePhotoClicked
  • onPictureTaken
  @Override
  public void onClick(View v)
  {
    switch(v.getId())
    {
    case R.id.takePhoto     : onTakePhotoClicked(v);                              
                              break;

    case R.id.savePhoto     : onPictureTaken(residencePhoto);    
                              break;                   
    }
  }

The approach we adopt is to use an implicit Intent to make use of the device camera (which we check is available). Here is the implementation of onTakePhotoClicked:

  public void onTakePhotoClicked(View v)
  {
    // Check for presence of device camera. If not present advise user and quit method.
    PackageManager pm = getPackageManager();
    if (!pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
      Toast.makeText(this, "Camera app not present on this device", Toast.LENGTH_SHORT).show();
      return;
    }
    // The device has a camera app ... so use it.
    Intent cameraIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
    startActivityForResult(cameraIntent,CAMERA_RESULT);
    savePhoto.setEnabled(true);
  }

This requires an import statement for the Intent class:

import android.content.Intent;

Also necessary is a definition of the constant CAMERA_RESULT. The value 5 has been chosen arbitrarily.

  private static  final int CAMERA_RESULT = 5;
  public static final String EXTRA_PHOTO_FILENAME = "org.wit.myrent.photo.filename";

Next we define the method onPictureTaken. We are chosing the png format as this is the only format acceptable to the helper method writeBitMap.

  private void onPictureTaken(Bitmap data) {
    String filename = UUID.randomUUID().toString() + ".png";
    if (writeBitmap(this, filename, data) == true) {
      Intent intent = new Intent();
      intent.putExtra(EXTRA_PHOTO_FILENAME, filename);
      setResult(Activity.RESULT_OK, intent);
    }
    else {
      setResult(Activity.RESULT_CANCELED);
    }
    finish();
  }

This requires these import statements:

import android.graphics.Bitmap;
import static org.wit.android.helpers.FileIOHelper.writeBitmap;

We must also define the string EXTRA_PHOTO_FILENAME. Note that the arguments in Intent.putExtra comprise key-value pairs, the first argument always being a String.

  public static   final String  EXTRA_PHOTO_FILENAME = "org.wit.myrent.photo.filename";

A further instance variable is required to hold the picture in bitmap form:

  private Bitmap residencePhoto;

We retrieve the photo taken by the device camera in the onActivityResult method:

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data)
  {
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode)
    {
      case ResidenceCameraActivity.CAMERA_RESULT :
        if(data != null) {
          processImage(data);
        }
        else {
          Toast.makeText(this, "Camera failure: check simulated camera present emulator advanced settings",
                                Toast.LENGTH_LONG).show();
        }
        break;
    }
  }

For convenience, we have located the code in a private method, processImage:

  private void processImage(Intent data)
  {
    residencePhoto = (Bitmap) data.getExtras().get("data");
    if(residencePhoto == null)
    {
      Toast.makeText(this, "Attempt to take photo did not succeed", Toast.LENGTH_SHORT).show();
    }
    residenceImage.setImageBitmap(residencePhoto);
  }

Complete set import statements:

import org.wit.myrent.R;

import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

Residence Fragment

Here we provide instructions on how to wire up the camera module to the ResidenceFragment class.

Add imports:

import static org.wit.android.helpers.CameraHelper.showPhoto;
import android.widget.ImageView;

Add a static constant to represent the photo:

private static final int REQUEST_PHOTO = 0;

Create instance variables:

private ImageView cameraButton;
private ImageView photoView;

Invoke the showPhoto method in onStart.

  • Its purpose is to display the photo in the thumbnail if a photo exists.
showPhoto(getActivity(), residence, photoView);

Here, for reference, is the refactored onStart method:

  @Override
  public void onStart()
  {
    super.onStart();
    //display thumbnail photo
    showPhoto(getActivity(), residence, photoView);    
  }

In addListeners:

  • Bind the camera button and thumbnail placeholder to their respective layouts:
    cameraButton = (ImageView) v.findViewById(R.id.camera_button);
    photoView = (ImageView) v.findViewById(R.id.myrent_imageView);
  • Register listener for camera button:
    cameraButton.setOnClickListener(this);

In onActivityResult introduce code to:

  • capture the photo filename,
  • save this to the model and
  • invoke a method to display the photo in a thumbnail.

Observe that we are replacing the if-else structure with a switch statement:

  @Override
  public void onActivityResult(int requestCode, int resultCode, Intent data)
  {
    if (resultCode != Activity.RESULT_OK)
    {
      return;
    }
    switch (requestCode)
    {
      case REQUEST_CONTACT:
        String name = ContactHelper.getContact(getActivity(), data);
        residence.tenant = name;
        tenantButton.setText(name);
        break;
      case REQUEST_PHOTO:
        String filename = data.getStringExtra(ResidenceCameraActivity.EXTRA_PHOTO_FILENAME);
        if (filename != null)
        {
          residence.photo = filename;
          showPhoto(getActivity(), residence, photoView );
        }
        break;
    }
  }

Respond to camera button click: add this code to onClick:

      case R.id.camera_button:
        Intent ic = new Intent(getActivity(), ResidenceCameraActivity.class);
        startActivityForResult(ic, REQUEST_PHOTO);
        break;

Gallery

Add a new layout file for the gallery:

File: res/layout/residence_gallery.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/FrameLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="org.wit.myrent.activities.ResidenceCameraActivity" >

    <ImageView
        android:id="@+id/residenceGalleryImage"
        android:layout_width="match_parent"
        android:layout_height="426dp"
        android:src="@mipmap/ic_launcher" />

</FrameLayout>

Here is the activity code:

File: org.wit.myrent.activities/ResidenceGalleryActivity.java

package org.wit.myrent.activities;

import org.wit.myrent.R;
import org.wit.myrent.app.MyRentApp;
import org.wit.myrent.models.Portfolio;
import org.wit.myrent.models.Residence;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.MenuItem;
import android.widget.ImageView;
import static org.wit.android.helpers.CameraHelper.showPhoto;

public class ResidenceGalleryActivity extends AppCompatActivity
{

  public static   final String  EXTRA_PHOTO_FILENAME = "org.wit.myrent.photo.filename";
  private ImageView photoView;

  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.residence_gallery);
    photoView = (ImageView) findViewById(R.id.residenceGalleryImage);
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    showPicture();
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item)
  {
    switch (item.getItemId())
    {
    case android.R.id.home  : onBackPressed();
                              return true;
    default                 : return super.onOptionsItemSelected(item);
    }
  }

  private void showPicture()
  {
    Long resId = (Long)getIntent().getSerializableExtra(ResidenceFragment.EXTRA_RESIDENCE_ID);
    MyRentApp app = (MyRentApp) getApplication();
    Portfolio portfolio = app.portfolio;
    Residence residence = portfolio.getResidence(resId);
    showPhoto(this, residence,  photoView);
  }
}

The gallery activity is started by a long-press of the thumbnail in the residence fragment. Here are the necessary changes to ResidenceFragment:

  • Add an OnLongClickListener interface to those already implemented, the purpose being to listen for a long-press on the thumbnail image.

public class ResidenceFragment extends Fragment implements 
    TextWatcher,
    OnCheckedChangeListener,
    OnClickListener,
    DatePickerDialog.OnDateSetListener,
    View.OnLongClickListener
  • Register the listener in the addListeners method:
    photoView.setOnLongClickListener(this);
  • Implement the long-press listener method:
  /* ====================== longpress thumbnail ===================================*/
  /*
   * Long press the bitmap image to view photo in single-photo gallery
   */
  @Override
  public boolean onLongClick(View v)
  {
    Intent i = new Intent(getActivity(), ResidenceGalleryActivity.class);
    i.putExtra(EXTRA_RESIDENCE_ID, residence.id);
    startActivity(i);
    return true;
  }

ResidenceFragment

For reference, here is the refactored class:

package org.wit.myrent.activities;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import org.wit.android.helpers.ContactHelper;
import org.wit.android.helpers.MapHelper;
import org.wit.myrent.R;
import org.wit.myrent.app.MyRentApp;
import org.wit.myrent.models.Portfolio;
import org.wit.myrent.models.Residence;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.app.DatePickerDialog;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.DatePicker;
import android.widget.EditText;

import static org.wit.android.helpers.ContactHelper.sendEmail;
import static org.wit.android.helpers.IntentHelper.navigateUp;
import static org.wit.android.helpers.IntentHelper.startActivityWithData;
import static org.wit.android.helpers.CameraHelper.showPhoto;

import android.widget.ImageView;


public class ResidenceFragment extends Fragment implements TextWatcher,
    OnCheckedChangeListener,
    OnClickListener,
    DatePickerDialog.OnDateSetListener,
    View.OnLongClickListener
{
  public static final String EXTRA_RESIDENCE_ID = "myrent.RESIDENCE_ID";
  private static final int REQUEST_CONTACT = 1;
  private static final int REQUEST_PHOTO = 0;


  private EditText geolocation;
  private CheckBox rented;
  private Button dateButton;
  private Button tenantButton;
  private Button reportButton;

  private Residence residence;
  private Portfolio portfolio;

  String emailAddress = "";

  private ImageView cameraButton;
  private ImageView photoView;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setHasOptionsMenu(true);

    UUID resId = (UUID) getActivity().getIntent().getSerializableExtra(EXTRA_RESIDENCE_ID);

    MyRentApp app = (MyRentApp) getActivity().getApplication();
    portfolio = app.portfolio;
    residence = portfolio.getResidence(resId);

  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
    super.onCreateView(inflater, parent, savedInstanceState);
    View v = inflater.inflate(R.layout.fragment_residence, parent, false);

    addListeners(v);
    updateControls(residence);

    FloatingActionButton fab = (FloatingActionButton) v.findViewById(R.id.fab);
    fab.setOnClickListener(this);

    return v;
  }

  private void addListeners(View v) {
    geolocation = (EditText) v.findViewById(R.id.geolocation);
    dateButton = (Button) v.findViewById(R.id.registration_date);
    rented = (CheckBox) v.findViewById(R.id.isrented);
    tenantButton = (Button) v.findViewById(R.id.tenant);
    reportButton = (Button) v.findViewById(R.id.residence_reportButton);
    cameraButton = (ImageView) v.findViewById(R.id.camera_button);
    photoView = (ImageView) v.findViewById(R.id.myrent_imageView);


    geolocation.addTextChangedListener(this);
    dateButton.setOnClickListener(this);
    rented.setOnCheckedChangeListener(this);
    tenantButton.setOnClickListener(this);
    reportButton.setOnClickListener(this);
    cameraButton.setOnClickListener(this);
    photoView.setOnLongClickListener(this);
  }

  public void updateControls(Residence residence) {
    geolocation.setText(residence.geolocation);
    rented.setChecked(residence.rented);
    dateButton.setText(residence.getDateString());
  }

  @Override
  public void onStart() {
    super.onStart();
    //display thumbnail photo
    showPhoto(getActivity(), residence, photoView);
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
      case android.R.id.home:
        navigateUp(getActivity());
        return true;
      default:
        return super.onOptionsItemSelected(item);
    }
  }

  @Override
  public void onPause() {
    super.onPause();
    portfolio.saveResidences();
  }

  @Override
  public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (resultCode != Activity.RESULT_OK) {
      return;
    }
    switch (requestCode) {
      case REQUEST_CONTACT:
        String name = ContactHelper.getContact(this, data);
        emailAddress = ContactHelper.getEmail(this, data);
        tenantButton.setText(name + " : " + emailAddress);
        residence.tenant = name;
        break;
      case REQUEST_PHOTO:
        String filename = data.getStringExtra(ResidenceCameraActivity.EXTRA_PHOTO_FILENAME);
        if (filename != null) {
          residence.photo = filename;
          showPhoto(getActivity(), residence, photoView);
        }
        break;
    }
  }

  @Override
  public void beforeTextChanged(CharSequence s, int start, int count, int after) {
  }

  @Override
  public void onTextChanged(CharSequence s, int start, int before, int count) {
  }

  @Override
  public void afterTextChanged(Editable c) {
    Log.i(this.getClass().getSimpleName(), "geolocation " + c.toString());
    residence.geolocation = c.toString();
  }

  @Override
  public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    residence.rented = isChecked;
  }

  @Override
  public void onClick(View v) {
    switch (v.getId()) {
      case R.id.registration_date:
        Calendar c = Calendar.getInstance();
        DatePickerDialog dpd = new DatePickerDialog(getActivity(), this, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH));
        dpd.show();
        break;

      case R.id.tenant:
        Intent i = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
        startActivityForResult(i, REQUEST_CONTACT);
        if (residence.tenant != null) {
          tenantButton.setText("Tenant: " + residence.tenant);
        }
        break;

      case R.id.residence_reportButton:
        sendEmail(getActivity(), "emailAddress", getString(R.string.residence_report_subject), residence.getResidenceReport(getActivity()));
        break;

      case R.id.fab:
        startActivityWithData(getActivity(), MapActivity.class, EXTRA_RESIDENCE_ID, residence.id);
        break;

      case R.id.camera_button:
        Intent ic = new Intent(getActivity(), ResidenceCameraActivity.class);
        startActivityForResult(ic, REQUEST_PHOTO);
        break;
    }
  }

  @Override
  public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
    Date date = new GregorianCalendar(year, monthOfYear, dayOfMonth).getTime();
    residence.date = date;
    dateButton.setText(residence.getDateString());
  }

  /* ====================== longpress thumbnail ===================================*/
  /*
   * Long press the bitmap image to view photo in single-photo gallery
   */
  @Override
  public boolean onLongClick(View v)
  {
    Intent i = new Intent(getActivity(), ResidenceGalleryActivity.class);
    i.putExtra(EXTRA_RESIDENCE_ID, residence.id);
    startActivity(i);
    return true;
  }
}

Test

Test the app as follows:

  • Build and install on device or emulator
  • Create a new residence
  • Click the photo button
  • Take a photo
  • Save the photo
  • Verify the photo in thumbnail in details view
  • Long-press the thumbnail: does photo display in gallery?
  • Return from gallery to details view using up button.
  • Switch to residence list and back to details view: is picture still displayed?
  • Shutdown the app and relaunch: is picture still displayed in thumbnail?

The application at the end of this lab is available for reference here: myrent-15

Camera Permissions

This file contains the gist of the code required to determine if permission to use the camera is to be allowed or denied at runtime.

package org.wit.mytweet.activities;

import android.support.v4.app.ActivityCompat;
...
...

public class MyTweetCameraActivity  extends AppCompatActivity implements View.OnClickListener {


    public static final int CAMERA_PERMISSION = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...

    }


    @Override
    public void onClick(View v) {

        switch(v.getId())
        {
            case R.id.takePhoto:
                /*-- jf 24-12-2015--*/
                checkCameraPermission();
                //onTakePhotoClicked(v);
                break;

            ...
            ...
        }

    }

    public void onTakePhotoClicked()  /*-- jf 24-12-2015-- removed redundant parameter*/
    {
        ...
        ...
    }

    /*-- jf 24-12-2015--*/
    /**
     * http://stackoverflow.com/questions/32714787/android-m-permissions-onrequestpermissionsresult-not-being-called
     * This is an override of AppCompat.onRequestPermissionsResult
     *
     * @param requestCode
     * @param permissions String array of permissions requested.
     * @param grantResults int array of results for permissions request.
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            case MyTweetCameraActivity.CAMERA_RESULT : {
                // If request is cancelled, the result arrays are empty.
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

                    onTakePhotoClicked();
                }
                break;
            }
        }
    }

    /**
     * Bespoke method to check if camera permission exists.
     * If it exists then the photo taken.
     * Otherwise, the method AppCompat.request permissions is invoked and
     * The response is via the callback onRequestPermissionsResult.
     * In onRequestPermissionsResult, on successfully being granted permission photo taken.
     * See: https://developer.android.com/training/permissions/requesting.html
     * See: http://bit.ly/2hBUAZD
     */
    private void checkCameraPermission() {
        if (ContextCompat.checkSelfPermission(this,
            Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {

            onTakePhotoClicked();
        }
        else {
            // Invoke callback to request user-granted permission
            ActivityCompat.requestPermissions( // If this file represents fragment replace ActivityCompat with android.support.v13.app.FragmentCompat;
                new String[]{Manifest.permission.CAMERA},
                CAMERA_PERMISSION);
        }
    }
    /*-- jf 24-12-2015--end--*/
}

Exercises

  1. When a residence is deleted, delete its locally-saved photo (png) if it exists.

Challenges

  1. When a photo is taken and saved:

  2. When a residence is deleted, delete the photo on the server.